package com.wt.springsamples.security.config;

import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.log.LogMessage;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.method.AbstractMethodSecurityMetadataSource;
import org.springframework.security.access.prepost.*;
import org.springframework.util.ClassUtils;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;

public class MyPrePostAnnotationSecurityMetadataSource extends AbstractMethodSecurityMetadataSource {
    private final PrePostInvocationAttributeFactory attributeFactory;
    public MyPrePostAnnotationSecurityMetadataSource(PrePostInvocationAttributeFactory attributeFactory) {
        this.attributeFactory = attributeFactory;
    }

    @Override
    public Collection<ConfigAttribute> getAttributes(Method method, Class<?> targetClass) {
        if (method.getDeclaringClass() == Object.class) {
            return Collections.emptyList();
        }
        PreFilter preFilter = findAnnotation(method, targetClass, PreFilter.class);
        PreAuthorize preAuthorize = findAnnotation(method, targetClass, PreAuthorize.class);
        PostFilter postFilter = findAnnotation(method, targetClass, PostFilter.class);
        // TODO: Can we check for void methods and throw an exception here?
        PostAuthorize postAuthorize = findAnnotation(method, targetClass, PostAuthorize.class);
        if (preFilter == null && preAuthorize == null && postFilter == null && postAuthorize == null) {
            // There is no meta-data so return
            return Collections.emptyList();
        }
        String preFilterAttribute = (preFilter != null) ? preFilter.value() : null;
        String filterObject = (preFilter != null) ? preFilter.filterTarget() : null;
        String preAuthorizeAttribute = (preAuthorize != null) ? preAuthorize.value() : null;
        String postFilterAttribute = (postFilter != null) ? postFilter.value() : null;
        String postAuthorizeAttribute = (postAuthorize != null) ? postAuthorize.value() : null;
        ArrayList<ConfigAttribute> attrs = new ArrayList<>(2);
        PreInvocationAttribute pre = this.attributeFactory.createPreInvocationAttribute(preFilterAttribute,
                filterObject, preAuthorizeAttribute);
        if (pre != null) {
            attrs.add(pre);
        }
        PostInvocationAttribute post = this.attributeFactory.createPostInvocationAttribute(postFilterAttribute,
                postAuthorizeAttribute);
        if (post != null) {
            attrs.add(post);
        }
        attrs.trimToSize();
        return attrs;
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    private <A extends Annotation> A findAnnotation(Method method, Class<?> targetClass, Class<A> annotationClass) {

        Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
        A annotation = AnnotatedElementUtils.findMergedAnnotation(specificMethod, annotationClass);
        if (annotation != null) {
            this.logger.debug(LogMessage.format("%s found on specific method: %s", annotation, specificMethod));
            return annotation;
        }
        // Check the original (e.g. interface) method
        if (specificMethod != method) {
            annotation = AnnotatedElementUtils.findMergedAnnotation(method, annotationClass);
            if (annotation != null) {
                this.logger.debug(LogMessage.format("%s found on: %s", annotation, method));
                return annotation;
            }
        }
        // Check the class-level (note declaringClass, not targetClass, which may not
        // actually implement the method)
        annotation = AnnotatedElementUtils.findMergedAnnotation(specificMethod.getDeclaringClass(), annotationClass);
        if (annotation != null) {
            this.logger.debug(
                    LogMessage.format("%s found on: %s", annotation, specificMethod.getDeclaringClass().getName()));
            return annotation;
        }
        return null;
    }
}
